package csc411;

public class Message {

	int requestId;
	int methodId;
	String name;
	String password;
	int accountNum;
	Currency type;
	float amount;
	int accountNumToTransfer;
	byte[] data;
	byte[] messageInfo;
	int interval;

	public Message() {
		this.requestId = -1;
		this.methodId = -1;
		this.name = null;
		this.password = null;
		this.accountNum = -1;
		this.type = null;
		this.amount = -1;
		this.accountNumToTransfer = -1;
		this.data = null;
		this.messageInfo = null;
		this.interval = -1;
	}

	public Message(byte[] data) {
		this();
		this.data = data;
	}

	// open new account
	public Message(int requestId, int methodId, String name,
			String password, Currency type, float balance) {

		this();
		this.requestId = requestId;
		this.methodId = methodId;
		this.name = name;
		this.password = password;
		this.type = type;
		this.amount = balance;

		this.pack(requestId, methodId, name, password, type, balance);

	}

	// close existing account & check balance
	public Message(int requestId, int methodId, String name,
			String password, int accountNum) {

		this();
		this.requestId = requestId;
		this.methodId = methodId;
		this.name = name;
		this.password = password;
		this.accountNum = accountNum;


		this.pack(requestId, methodId, name, password, accountNum);

	}

	// deposit/withdraw
	public Message(int requestId, int methodId, String name,
			String password, int accountNum, Currency type, float amount) {

		this();
		this.requestId = requestId;
		this.methodId = methodId;
		this.name = name;
		this.password = password;
		this.accountNum = accountNum;
		this.type = type;
		this.amount = amount;


		this.pack(requestId, methodId, name, password, accountNum, type, amount);

	}

	// transfer
	public Message(int requestId, int methodId, String name,
			String password, int accountNum, Currency type, float amount, int accountNumToTransfer) {

		this();
		this.requestId = requestId;
		this.methodId = methodId;
		this.name = name;
		this.password = password;
		this.accountNum = accountNum;
		this.type = type;
		this.amount = amount;
		this.accountNumToTransfer = accountNumToTransfer;


		this.pack(requestId, methodId, name, password, accountNum, type, amount, accountNumToTransfer);

	}

	// FEEZ 160310
	// register client for monitoring updates
	public Message(int requestId, int methodId, int interval) {

		this();
		this.interval = interval;

		this.pack(requestId, methodId, interval);

	}

	// FEEZ 140310
	private byte[] packMessageInfo(int endianFormat, int messageType, int retransmission) {
		byte[] b = new byte[4];

		// set all bytes to zero
		b[0] = 0;
		b[1] = 0;
		b[2] = 0;
		b[3] = 0;

		Utils.setBit(b, 0, endianFormat);
		Utils.setBit(b, 1, messageType);
		Utils.setBit(b, 2, retransmission);

		// [message info] 4 bytes
		//		bit 0 - endian format	(0 - big-endian, 1 - little-endian)
		//		bit 1 - message type	(0 - request, 1 - reply)
		//		bit 2 - retransmit		(0 - no, 1 - retransmit)
		//		bit 3-31 - currently unused

		this.messageInfo = b;
		return b;

	}

	// overloaded build message operations
	// Riz 230310 for open account
	private byte[] pack(int requestId, int methodId, String name,
			String password, Currency type, float balance) {

		// the message will be in the following structure:
		// [message info] 4 bytes
		// bit 0 - endian format (0 - big-endian, 1 - little-endian)
		// bit 1 - message type	(0 - request, 1 - reply)
		// bit 2 - retransmit (0 - no, 1 - retransmit)
		// [request id] int 4 bytes
		// [method id] int 4 bytes
		// [name] variable string at least 8 bytes
		// [password] fixed string 4 bytes
		// [type] currency 4 bytes
		// [balance] float 4 bytes

		byte[] message = new byte[1024];

		// message info defaults for new messages
		int endianFormat = 0, // java is big-endian
				messageType = 0, // request type
				retransmit = 0;	// not a retransmission

		// prepare message contents
		byte[] b_info = this.packMessageInfo(endianFormat, messageType, retransmit);
		byte[] b_requestId = Utils.intToBytes(requestId);
		byte[] b_method = Utils.intToBytes(methodId);
		byte[] b_name = Utils.stringToBytes(name);
		byte[] b_password = Utils.fixedStringToBytes(password);
		byte[] b_type = Utils.currencyToByte(type);
		byte[] b_balance = Utils.floatToBytes(balance);

		// pack message contents
		int position = 0;
		System.arraycopy(b_info, 0, message, position, b_info.length);

		position += b_info.length;
		System.arraycopy(b_requestId, 0, message, position, b_requestId.length);

		position += b_requestId.length;
		System.arraycopy(b_method, 0, message, position, b_method.length);

		position += b_method.length;
		System.arraycopy(b_name, 0, message, position, b_name.length);

		position += b_name.length;
		System.arraycopy(b_password, 0, message, position, b_password.length);

		position += b_password.length;
		System.arraycopy(b_type, 0, message, position, b_type.length);

		position += b_type.length;
		System.arraycopy(b_balance, 0, message, position, b_balance.length);

		// we keep this message (byte array) for retransmission later
		this.data = message;

		return message;
	}

	// Nadiah (220210:1620)
	private byte[] pack(int requestId, int methodId, String name,
			String password, int accountNum) {

		// the message will be in the following structure:
		// [message info] 4 bytes
		//	bit 0 - endian format	(0 - big-endian, 1 - little-endian)
		//	bit 1 - message type	(0 - request, 1 - reply)
		//	bit 2 - retransmit	(0 - no, 1 - retransmit)
		// [request id] int	4 bytes
		// [method id] int 4 bytes
		// [name] variable string at least 8 bytes
		// [password] fixed string 4 bytes
		// [account num] int 4 bytes

		byte[] message = new byte[1024];

		// message info defaults for new messages
		int endianFormat = 0, // java is big-endian
				messageType = 0, // request type
				retransmit = 0;	// not a retransmission

		// prepare message contents
		byte[] b_info = this.packMessageInfo(endianFormat, messageType, retransmit);
		byte[] b_requestId = Utils.intToBytes(requestId);
		byte[] b_method = Utils.intToBytes(methodId);
		byte[] b_name = Utils.stringToBytes(name);
		byte[] b_password = Utils.fixedStringToBytes(password);
		byte[] b_accountNum = Utils.intToBytes(accountNum);

		// pack message contents
		int position = 0;
		System.arraycopy(b_info, 0, message, position, b_info.length);

		position += b_info.length;
		System.arraycopy(b_requestId, 0, message, position, b_requestId.length);

		position += b_requestId.length;
		System.arraycopy(b_method, 0, message, position, b_method.length);

		position += b_method.length;
		System.arraycopy(b_name, 0, message, position, b_name.length);

		position += b_name.length;
		System.arraycopy(b_password, 0, message, position, b_password.length);

		position += b_password.length;
		System.arraycopy(b_accountNum, 0, message, position, b_accountNum.length);

		// we keep this message (byte array) for retransmission later
		this.data = message;

		return message;
	}

	// FEEX 140310 updated to include type and amount
	private byte[] pack(int requestId, int methodId, String name,
			String password, int accountNum, Currency type, float amount) {

		// the message will be in the following structure:
		// [message info] 4 bytes
		//		bit 0 - endian format	(0 - big-endian, 1 - little-endian)
		//		bit 1 - message type	(0 - request, 1 - reply)
		//		bit 2 - retransmit		(0 - no, 1 - retransmit)
		// [request id] int	4 bytes
		// [method id] int 4 bytes
		// [name] variable string at least 8 bytes
		// [password] fixed string 4 bytes
		// [account num] int 4 bytes
		// [type] currency 4 bytes
		// [amount] float 4 bytes

		byte[] message = new byte[1024];

		// message info defaults for new messages
		int endianFormat = 0, // java is big-endian
				messageType = 0, // request type
				retransmit = 0;		// not a retransmission

		// prepare message contents
		byte[] b_info = this.packMessageInfo(endianFormat, messageType, retransmit);
		byte[] b_requestId = Utils.intToBytes(requestId);
		byte[] b_method = Utils.intToBytes(methodId);
		byte[] b_name = Utils.stringToBytes(name);
		byte[] b_password = Utils.fixedStringToBytes(password);
		byte[] b_accountNum = Utils.intToBytes(accountNum);
		byte[] b_type = Utils.currencyToByte(type);
		byte[] b_amount = Utils.floatToBytes(amount);

		// pack message contents
		int position = 0;
		System.arraycopy(b_info, 0, message, position, b_info.length);

		position += b_info.length;
		System.arraycopy(b_requestId, 0, message, position, b_requestId.length);

		position += b_requestId.length;
		System.arraycopy(b_method, 0, message, position, b_method.length);

		position += b_method.length;
		System.arraycopy(b_name, 0, message, position, b_name.length);

		position += b_name.length;
		System.arraycopy(b_password, 0, message, position, b_password.length);

		position += b_password.length;
		System.arraycopy(b_accountNum, 0, message, position, b_accountNum.length);

		position += b_accountNum.length;
		System.arraycopy(b_type, 0, message, position, b_type.length);

		position += b_type.length;
		System.arraycopy(b_amount, 0, message, position, b_amount.length);

		// uncomment below to see message contents
/*
		System.out.print("Endian-format in bytes:");
		Utils.printBytes(b_info);

		System.out.print("Request ID in bytes:");
		Utils.printBytes(b_requestId);

		System.out.print("Method ID in bytes:");
		Utils.printBytes(b_method);

		System.out.print("Name in bytes:");
		Utils.printBytes(b_name);

		System.out.print("Password in bytes:");
		Utils.printBytes(b_password);

		System.out.print("Acct# in bytes:");
		Utils.printBytes(b_accountNum);

		System.out.print("Type in bytes:");
		Utils.printBytes(b_type);

		System.out.print("Amount in bytes:");
		Utils.printBytes(b_amount);

		System.out.print("Message in bytes:");
		Utils.printBytes(message);
		 */
		// TODO: ensure the total message size is less than 1024 bytes
		// since other items are fixed in length, use string length to check
		// but then again, 1024 bytes is more than enough.

		// we keep this message (byte array) for retransmission later
		this.data = message;

		return message;

	}

	// Nadiah (220310:1950)
	private byte[] pack(int requestId, int methodId, String name,
			String password, int accountNum, Currency type, float amount, int accountNumToTransfer) {

		// the message will be in the following structure:
		// [message info] 4 bytes
		//		bit 0 - endian format	(0 - big-endian, 1 - little-endian)
		//		bit 1 - message type	(0 - request, 1 - reply)
		//		bit 2 - retransmit		(0 - no, 1 - retransmit)
		// [request id] int	4 bytes
		// [method id] int 4 bytes
		// [name] variable string at least 8 bytes
		// [password] fixed string 4 bytes
		// [account num] int 4 bytes
		// [type] currency 4 bytes
		// [amount] float 4 bytes
		// [account num to transfer] int 4 bytes

		byte[] message = new byte[1024];

		// message info defaults for new messages
		int endianFormat = 0, // java is big-endian
				messageType = 0, // request type
				retransmit = 0;		// not a retransmission

		// prepare message contents
		byte[] b_info = this.packMessageInfo(endianFormat, messageType, retransmit);
		byte[] b_requestId = Utils.intToBytes(requestId);
		byte[] b_method = Utils.intToBytes(methodId);
		byte[] b_name = Utils.stringToBytes(name);
		byte[] b_password = Utils.fixedStringToBytes(password);
		byte[] b_accountNum = Utils.intToBytes(accountNum);
		byte[] b_type = Utils.currencyToByte(type);
		byte[] b_amount = Utils.floatToBytes(amount);
		byte[] b_accountNumToTransfer = Utils.intToBytes(accountNumToTransfer);

		// pack message contents
		int position = 0;
		System.arraycopy(b_info, 0, message, position, b_info.length);

		position += b_info.length;
		System.arraycopy(b_requestId, 0, message, position, b_requestId.length);

		position += b_requestId.length;
		System.arraycopy(b_method, 0, message, position, b_method.length);

		position += b_method.length;
		System.arraycopy(b_name, 0, message, position, b_name.length);

		position += b_name.length;
		System.arraycopy(b_password, 0, message, position, b_password.length);

		position += b_password.length;
		System.arraycopy(b_accountNum, 0, message, position, b_accountNum.length);

		position += b_accountNum.length;
		System.arraycopy(b_type, 0, message, position, b_type.length);

		position += b_type.length;
		System.arraycopy(b_amount, 0, message, position, b_amount.length);

		position += b_amount.length;
		System.arraycopy(b_accountNumToTransfer, 0, message, position, b_accountNumToTransfer.length);

		// we keep this message (byte array) for retransmission later
		this.data = message;

		return message;

	}

	// FEEZ 160310
	private byte[] pack(int requestId, int methodId, int interval) {

		// the message will be in the following structure:
		// [message info] 4 bytes
		//		bit 0 - endian format	(0 - big-endian, 1 - little-endian)
		//		bit 1 - message type	(0 - request, 1 - reply)
		//		bit 2 - retransmit		(0 - no, 1 - retransmit)
		// [request id] int	4 bytes
		// [method id] int 4 bytes
		// [interval] int 4 bytes

		byte[] message = new byte[1024];

		// message info defaults for new messages
		int endianFormat = 0, // java is big-endian
				messageType = 0, // request type
				retransmit = 0;		// not a retransmission

		// prepare message contents
		byte[] b_info = this.packMessageInfo(endianFormat, messageType, retransmit);
		byte[] b_requestId = Utils.intToBytes(requestId);
		byte[] b_method = Utils.intToBytes(methodId);
		byte[] b_interval = Utils.intToBytes(interval);

		// pack message contents
		int position = 0;
		System.arraycopy(b_info, 0, message, position, b_info.length);

		position += b_info.length;
		System.arraycopy(b_requestId, 0, message, position, b_requestId.length);

		position += b_requestId.length;
		System.arraycopy(b_method, 0, message, position, b_method.length);

		position += b_method.length;
		System.arraycopy(b_interval, 0, message, position, b_interval.length);

		// we keep this message (byte array) for retransmission later
		this.data = message;

		return message;

	}

	// FEEZ 140310
	public byte[] markRetransmit() {
		// update the retransmission bit in the message (this.data)
		// and return it

		// note: retransmission bit is bit 2 (LSB) of the first 4 bytes

		byte[] b_messageInfo = new byte[4];

		System.arraycopy(this.data, 0, b_messageInfo, 0, 4);

		// set retransmission bit (bit 2)
		Utils.setBit(b_messageInfo, 2, 1);

		System.arraycopy(b_messageInfo, 0, this.data, 0, 4);

		return this.data;
	}

	// TODO
	public void unpack() {

		// if message type == reply
		//		message contents are simply a string (server response)
		// else message type == request,
		//		unpack based on methodId



		// extract request id
		this.extractRequestId();

		// extract method id
		this.extractMethodId();

		// Riz 230310
		if (this.methodId == 1) { //open account
			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract name
			this.extractName(position);

			// calculate position of password which is after a variable string
			int nameLength = Utils.calcStringLength(this.name.length());
			position = position + 4 + nameLength;	// new position

			// extract password
			this.extractPassword(position);
			position += 4;

			// extract type
			this.extractCurrencyType(position);
			position += 4;

			// extract balance
			this.extractAmount(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes

		} else if (this.methodId == 2) { //check balance
			//Riz 230310
			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract name
			this.extractName(position);

			// calculate position of password which is after a variable string
			int nameLength = Utils.calcStringLength(this.name.length());
			position = position + 4 + nameLength;	// new position

			// extract password
			this.extractPassword(position);
			position += 4;

			// extract account num to check
			this.extractAccountNum(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes


		} else if (this.methodId == 3) {

			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract name
			this.extractName(position);

			// calculate position of password which is after a variable string
			int nameLength = Utils.calcStringLength(this.name.length());
			position = position + 4 + nameLength;	// new position

			// extract password
			this.extractPassword(position);
			position += 4;

			// extract account num
			this.extractAccountNum(position);
			position += 4;

			// extract type
			this.extractCurrencyType(position);
			position += 4;

			// extract balance
			this.extractAmount(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes

		} else if (this.methodId == 5) { //check balance
			//Riz 230310
			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract name
			this.extractName(position);

			// calculate position of password which is after a variable string
			int nameLength = Utils.calcStringLength(this.name.length());
			position = position + 4 + nameLength;	// new position

			// extract password
			this.extractPassword(position);
			position += 4;

			// extract account num to check
			this.extractAccountNum(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes


		} else if (this.methodId == 6) {
			// Nadiah (220310:2000)

			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract name
			this.extractName(position);

			// calculate position of password which is after a variable string
			int nameLength = Utils.calcStringLength(this.name.length());
			position = position + 4 + nameLength;	// new position

			// extract password
			this.extractPassword(position);
			position += 4;

			// extract account num
			this.extractAccountNum(position);
			position += 4;

			// extract type
			this.extractCurrencyType(position);
			position += 4;

			// extract balance
			this.extractAmount(position);
			position += 4;

			// extract account num to transfer
			this.extractAccountNumToTransfer(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes

		} else if (this.methodId == 7) {

			// FEEZ 160310

			int position = 12;	// the first content is at position 12

			// extract message info
			this.extractMessageInfo();

			// extract interval
			this.extractInterval(position);

			// and we're done unpacking the message for this method.
			// all of the message contents should be in the message attributes

		} else {
			this.data = (new String("Corrupted message: Unknown method call")).getBytes();
		}

	}

	// extract message type from data byte array
	private byte[] extractMessageInfo() {

		if (this.data == null) {
			return null;
		}

		byte[] b_messageInfo = new byte[4];

		System.arraycopy(this.data, 0, b_messageInfo, 0, 4);

		// store request id in the message
		this.messageInfo = b_messageInfo;

		return b_messageInfo;

	}

	// extract request id from data byte array
	private int extractRequestId() {

		if (this.data == null) {
			return -1;
		}

		int value = 0;
		byte[] b_requestId = new byte[4];

		System.arraycopy(this.data, 4, b_requestId, 0, 4);

		value = Utils.bytesToInt(b_requestId);

		// store request id in the message
		this.requestId = value;

		return value;

	}

	// extract method id from data byte array
	private int extractMethodId() {

		if (this.data == null) {
			return -1;
		}

		int value = 0;
		byte[] b_methodId = new byte[4];

		System.arraycopy(this.data, 8, b_methodId, 0, 4);

		value = Utils.bytesToInt(b_methodId);

		// store method id in the message
		this.methodId = value;

		return value;

	}

	// extract name from data byte array
	private String extractName(int position) {

		if (this.data == null) {
			return "Uninitialized message error.";
		}

		int nameLength = 0;
		String value = "";
		byte[] b_nameLength = new byte[4];
		byte[] b_name;

		// get length of name string
		System.arraycopy(this.data, position, b_nameLength, 0, 4);
		nameLength = Utils.bytesToInt(b_nameLength);

		// get length + name byte array
		b_name = new byte[nameLength + 4];
		System.arraycopy(this.data, position, b_name, 0, nameLength + 4);

		value = Utils.bytesToString(b_name);

		// store name in the message
		this.name = value;

		return value;

	}

	// extract password from data byte array
	private String extractPassword(int position) {

		if (this.data == null) {
			return "Uninitialized message error.";
		}

		String value = "";
		byte[] b_password = new byte[4];

		// get password
		System.arraycopy(this.data, position, b_password, 0, 4);

		value = Utils.bytesToFixedString(b_password);

		// store password in the message
		this.password = value;

		return value;

	}

	// extract account number from data byte array
	private int extractAccountNum(int position) {

		if (this.data == null) {
			return -1;
		}

		int value = -1;
		byte[] b_accountNum = new byte[4];

		// get account number
		System.arraycopy(this.data, position, b_accountNum, 0, 4);

		value = Utils.bytesToInt(b_accountNum);
		// store account number in the message
		this.accountNum = value;

		return value;

	}

	// extract account number from data byte array
	private int extractAccountNumToTransfer(int position) {
		// Nadiah (230310:1440)
		if (this.data == null) {
			return -1;
		}

		int value = -1;
		byte[] b_accountNumToTransfer = new byte[4];

		// get account number
		System.arraycopy(this.data, position, b_accountNumToTransfer, 0, 4);

		value = Utils.bytesToInt(b_accountNumToTransfer);
		// store account number in the message
		this.accountNumToTransfer = value;

		return value;

	}

	// FEEZ 140310
	private Currency extractCurrencyType(int position) {

		if (this.data == null) {
			return null;
		}

		Currency value = null;
		byte[] b_type = new byte[4];

		// get account number
		System.arraycopy(this.data, position, b_type, 0, 4);

		value = Utils.byteToCurrency(b_type);
		// store type  in the message
		this.type = value;

		return value;

	}

	// FEEZ 140310
	private float extractAmount(int position) {

		if (this.data == null) {
			return -1f;
		}

		float value = -1f;
		byte[] b_amount = new byte[4];

		// get account number
		System.arraycopy(this.data, position, b_amount, 0, 4);

		value = Utils.bytesToFloat(b_amount);

		// store amount in the message
		this.amount = value;

		return value;
	}

	// FEEZ 160310
	// extract interval from data byte array
	private int extractInterval(int position) {

		if (this.data == null) {
			return -1;
		}

		int value = -1;
		byte[] b_interval = new byte[4];

		// get account number
		System.arraycopy(this.data, position, b_interval, 0, 4);

		value = Utils.bytesToInt(b_interval);
		// store account number in the message
		this.interval = value;

		return value;

	}

	// get message type from extracted message info
	public int getMessageType() {

		if (this.messageInfo == null) {
			return -1;
		}

		return Utils.getBit(this.messageInfo, 1);

	}

	// FEEZ 140310
	// get endian format from extracted message info
	public int getEndianFormat() {

		if (this.messageInfo == null) {
			return -1;
		}

		return Utils.getBit(this.messageInfo, 0);

	}

	// FEEZ 140310
	// get retransmission bit from extracted message info
	public int getRetransmissionBit() {

		if (this.messageInfo == null) {
			return -1;
		}

		return Utils.getBit(this.messageInfo, 2);

	}
}
